package ru.averta.notes.service.search;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import ru.averta.notes.dao.GoogleSuggestDao;
import ru.averta.notes.dao.TagDao;
import ru.averta.notes.dao.UrlDao;
import ru.averta.notes.domain.GoogleSuggest;
import ru.averta.notes.domain.ajax.AjaxTagsSuggestAnswer;
import ru.averta.notes.domain.virtual.TagsCombination;
import ru.averta.notes.other.UtilsCommon;
import ru.averta.notes.service.UrlService;
import ru.averta.notes.service.math.DistanceResult;
import ru.averta.notes.service.math.EditDistance;
import ru.averta.notes.service.security.SecurityService;
import ru.averta.notes.service.tag.TagManager;
import ru.averta.notes.service.tag.TagsSuggestService;

import java.util.*;

/**
 * <code>GoogleSuggestService</code>
 *
 * @author fedor.belov (fedor.belov@mail.ru)
 *         Date: 26.09.11 16:05
 */

@Service("GoogleSuggestServiceImpl")
public class GoogleSuggestServiceImpl implements TagsSuggestService, GoogleSuggestService {

    private static final Logger logger = LoggerFactory.getLogger(GoogleSuggestServiceImpl.class);

    @Autowired private GoogleSuggestDao suggestDao;
    @Autowired private EditDistance distance;
    @Autowired private UrlService urlService;
    @Autowired private TagManager tagManager;
    @Autowired private UtilsCommon u;

    @Override
    public List<TagsCombination> suggestTags(String prefix, List<String> sourceTags) {

        //Строим "карту похожести" для всех текущих тегов - ключ тег, значение - коллекция похожих тегов
        Map<String, Collection<DistanceResult>> distanceMap = new HashMap<String, Collection<DistanceResult>>();
        if (u.checkNotEmpty(prefix))
            distanceMap.put(prefix, getDistanceResultsForTag(prefix, true));
        for (String tag : sourceTags)
            if (u.checkNotEmpty(tag)) distanceMap.put(tag, getDistanceResultsForTag(tag, false));

        return getTagCombinationsFor(prefix, sourceTags, distanceMap);
    }

    @Override
    public List<TagsCombination> suggestTags(String prefix, String... sourceTags) {
        return suggestTags(prefix, Arrays.asList(sourceTags));
    }

    /**
     * Возвращает коллекцию похожих на tag тегов
     * @param tag
     * @param isPrefix
     * @return
     */
    private Collection<DistanceResult> getDistanceResultsForTag(String tag, boolean isPrefix) {
        //TODO оптимизировать механизм. Не строить DameauLevenshteinDistanceResult для равных слов
        //TODO прекращать считать матрицу после преодоления max значения непохожести

        Collection<DistanceResult> result = new HashSet<DistanceResult>();

        //Отбираем все теги пользователя
        Collection<String> userTags = tagManager.findTagsByCUser();

        //Проходимся по всем тегам пользователя, смотрим насколько похож каждый
        for (String userTag : userTags) {
            DistanceResult distanceResult = distance.getDamerauLevenshteinDistanceResult(tag, userTag);
            if (
                    (distanceResult != null) &&
                            (isPrefix && distanceResult.isSufficientPrefixDistance()) ||
                            (!isPrefix && distanceResult.isSufficientDistance())) {
                result.add(distanceResult);
            }
        }

        return result;
    }

    /**
     * Для префикса prefix и тегов sourceTags из suggestsByTags строит итоговую комбинацию тегов
     * @param prefix
     * @param sourceTags
     * @param suggestsByTags
     * @return
     */
    private List<TagsCombination> getTagCombinationsFor(String prefix, List<String> sourceTags, Map<String, Collection<DistanceResult>> suggestsByTags) {
        List<TagsCombination> answer = new LinkedList<TagsCombination>();
        List<DistanceResult> results = new LinkedList<DistanceResult>();

        List<String> tagsToProcess = new LinkedList<String>(sourceTags);
        tagsToProcess.add(prefix);

        logger.debug("Для префикса {} и тегов {} возвращаем похожие записи {}", new Object[] {prefix, sourceTags, suggestsByTags});

        computeTagCombinations(answer, results, tagsToProcess, suggestsByTags);
        return answer;
    }

    /**
     * Вспомогательный метод необходимый для построения комбинации
     *
     * @param combinations
     * @param results
     * @param tagsToProcess
     * @param suggestsByTags
     */
    private void computeTagCombinations(List<TagsCombination> combinations, List<DistanceResult> results, List<String> tagsToProcess, Map<String, Collection<DistanceResult>> suggestsByTags) {
        if (tagsToProcess.size() > 0) {
            String tag = tagsToProcess.get(0);
            List<String> nextTags = new LinkedList<String>(tagsToProcess);
            nextTags.remove(0);

            Collection<DistanceResult> suggests = suggestsByTags.get(tag);
            if (suggests == null) {
                List<DistanceResult> list = new LinkedList<DistanceResult>(results);
                computeTagCombinations(combinations, list, nextTags, suggestsByTags);
            } else {
                for (DistanceResult suggest : suggests) {
                    List<DistanceResult> list = new LinkedList<DistanceResult>(results);
                    list.add(suggest);
                    computeTagCombinations(combinations, list, nextTags, suggestsByTags);
                }
            }

            return;
        }

        //Если есть сайты с данной комбинацией тегов, то добавляем ее в результат
        int count = getUrlsCountByTagsCombination(results);
        if (count > 0) {
            TagsCombination combination = new TagsCombination(count);
            for (DistanceResult suggest : results) {
                combination.addTag(suggest.getResultString(), suggest.getSameLettersBit());
            }
            combinations.add(combination);
        }
    }

    /**
     * Возвращает количество url'ов с данной комбинацией тегов
     *
     * @param suggestCombination
     * @return
     */
    private int getUrlsCountByTagsCombination(List<DistanceResult> suggestCombination) {
        Set<String> tags = new HashSet<String>();
        for (DistanceResult suggest : suggestCombination) {
            tags.add(suggest.getResultString());
        }

        Collection<Long> urls = urlService.getUrlsByTags(tags);
        int count = (urls == null) ? 0 : urls.size();

        logger.debug("Существует {} сайтов с комбинацией {} ", count, tags);

        return count;
    }

    @Override
    public List<GoogleSuggest> suggestPhrases(String prefix, Set<String> tags) {
        //todo кешируем результаты данного метода, когда длина префикса кратна трем или равна 1

        List<GoogleSuggest> result = new LinkedList<GoogleSuggest>();
        if (prefix == null || prefix.equals("")) return result;

        List<GoogleSuggest> sourceSuggest;

        /**
         * Если префикс - 1 символ, то исходный набор для фильтрации - все googleSuggest для данных тегов
         * Если префикс > 1 символа, то используем рекурсию, чтобы отобрать исходныый набор
         * Рекурсия позволяет избежать лишних вычислений благодаря кешированию
         */
        if ((prefix.length() == 1) || (prefix.length() % 3 == 0)) {
            if (tags == null || tags.isEmpty()) {
                sourceSuggest = getSuggestsByUser();
            } else {
                sourceSuggest = getSuggestsByTags(tags);
            }
        } else if (prefix.length() < 3) {
            sourceSuggest = suggestPhrases(prefix.substring(0, 1), tags);
        } else {
            sourceSuggest = suggestPhrases(prefix.substring(0, ((prefix.length() / 3)*3)), tags);
        }

        //Фильтруем исходный список
        for (GoogleSuggest suggest : sourceSuggest) {
            if (suggest.isSatisfyText(prefix)) result.add(suggest);
        }

        return result;
    }

    /**
     * Возвращает все GoogleSuggest "доступные" текущему пользователю
     * @return
     */
    private List<GoogleSuggest> getSuggestsByUser() {
        //todo закешировать метод
        return suggestDao.findSuggestsByUrls(urlService.findUrlsByCUser());
    }

    /**
     * Возвращает GoogleSuggest для сайтов с тегами tags
     * @param tags
     * @return
     */
    private List<GoogleSuggest> getSuggestsByTags(Set<String> tags) {
        return suggestDao.findSuggestsByUrls(urlService.getUrlsByTags(tags));
    }

}
